﻿using System;
using System.IO;
using System.Linq;
using System.Net;
using System.Reflection;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Json;
using System.ServiceModel.Channels;
using System.ServiceModel.Dispatcher;
using System.Xml;
using System.Xml.Serialization;
using SilverlightRestServices.Channels;
using SilverlightRestServices.Description;

namespace SilverlightRestServices.Dispatcher
{
    public class RestMessageFormatter : IClientMessageFormatter
    {
        private String m_OperationName;
        private String m_EndpointAddress;

        private String m_HttpMethod = "GET";
        private String m_UrlTemplate;
        private String m_QueryStringTemplate;
        private String m_ContentType;

        private Type m_BodyType;
        private Int32 m_BodyParameterIndex;
        private SerializationMethod m_Serialization;

        private Type m_ResponseType;
        private SerializationMethod m_Deserialization;

        private XmlSerializer m_XmlSerializer;
        private DataContractSerializer m_DataContractSerializer;
        private DataContractJsonSerializer m_JsonSerializer;

        private XmlSerializer m_XmlDeserializer;
        private DataContractSerializer m_DataContractDeserializer;
        private DataContractJsonSerializer m_JsonDeserializer;

        public RestMessageFormatter(Uri endpointAddress, ClientOperation operation)
        {
            this.m_OperationName = operation.Name;
            this.m_EndpointAddress = endpointAddress.ToString();

            Object[] attr = operation.BeginMethod.GetCustomAttributes(typeof(RestGetAttribute), false);

            if (operation.EndMethod != null && operation.EndMethod.ReturnType != null)
            {
                this.m_ResponseType = operation.EndMethod.ReturnType;
            }

            if (attr.Length > 0)
            {
                RestGetAttribute getAttr = (RestGetAttribute)attr[0];

                this.SetUrlTemplate(getAttr.Url);
                if (this.m_ResponseType != null && this.m_ResponseType != typeof(void))
                {
                    this.SetDeserializationMethod(this.m_ResponseType, getAttr.ResponseFormat);
                }
            }
            else
            {
                attr = operation.BeginMethod.GetCustomAttributes(typeof(RestInvokeAttribute), false);

                if (attr.Length > 0)
                {
                    RestInvokeAttribute invokeAttr = (RestInvokeAttribute)attr[0];

                    if (!String.IsNullOrEmpty(invokeAttr.Method))
                    {
                        this.m_HttpMethod = invokeAttr.Method;
                    }
                    else
                    {
                        this.m_HttpMethod = "POST";
                    }

                    this.SetUrlTemplate(invokeAttr.Url);

                    if (this.m_ResponseType != null && this.m_ResponseType != typeof(void))
                    {
                        this.SetDeserializationMethod(this.m_ResponseType, invokeAttr.ResponseFormat);
                    }

                    if (!String.IsNullOrEmpty(invokeAttr.BodyParameter))
                    {
                        this.m_ContentType = invokeAttr.ContentType;

                        ParameterInfo[] parameters = operation.BeginMethod.GetParameters();
                        for (Int32 i = 0; i < parameters.Length - 2; i++)
                        {
                            if (String.Equals(invokeAttr.BodyParameter, parameters[i].Name, StringComparison.OrdinalIgnoreCase))
                            {
                                this.m_BodyParameterIndex = i;
                                this.m_BodyType = parameters[i].ParameterType;
                            }
                        }

                        if (this.m_BodyType == null)
                        {
                            throw new InvalidOperationException("Body Parameter Not Found for Operation: " + operation.Name);
                        }

                        this.SetSerializationMethod(this.m_BodyType, invokeAttr.BodyFormat);
                    }
                }
            }
        }

        public Object DeserializeReply(Message message, Object[] parameters)
        {
            if (this.m_ResponseType != null && this.m_ResponseType != typeof(void))
            {
                Stream msgStream = ((RestMessageProperty)message.Properties[RestMessageProperty.Name]).MessageData;

                switch (this.m_Deserialization)
                {
                    case SerializationMethod.IXmlSerializable:
                        IXmlSerializable response = (IXmlSerializable)Activator.CreateInstance(this.m_ResponseType);
                        using (XmlReader reader = XmlReader.Create(msgStream))
                        {
                            response.ReadXml(reader);
                        }

                        return response;
                    case SerializationMethod.XmlSerializable:
                        return this.m_XmlDeserializer.Deserialize(msgStream);
                    case SerializationMethod.XmlDataContract:
                        return this.m_DataContractDeserializer.ReadObject(msgStream);
                    case SerializationMethod.JsonDataContract:
                        return this.m_JsonDeserializer.ReadObject(msgStream);
                    default:
                        throw new InvalidOperationException("Serialization Method Not Recognized.");
                }
            }
            else
            {
                return null;
            }
        }

        public Message SerializeRequest(MessageVersion messageVersion, Object[] parameters)
        {
            Message msg = Message.CreateMessage(MessageVersion.None, this.m_OperationName + "Request");
            Boolean suppressBody = true;

            if (this.m_BodyType != null && parameters[this.m_BodyParameterIndex] != null)
            {
                suppressBody = false;
                Object bodyObject = parameters[this.m_BodyParameterIndex];
                MemoryStream buffer = new MemoryStream();

                switch (this.m_Serialization)
                {
                    case SerializationMethod.IXmlSerializable:
                        using (XmlWriter writer = XmlWriter.Create(buffer))
                        {
                            ((IXmlSerializable)bodyObject).WriteXml(writer);
                            writer.Flush();
                        }

                        break;
                    case SerializationMethod.XmlSerializable:
                        this.m_XmlSerializer.Serialize(buffer, bodyObject);

                        break;
                    case SerializationMethod.XmlDataContract:
                        this.m_DataContractSerializer.WriteObject(buffer, bodyObject);

                        break;
                    case SerializationMethod.JsonDataContract:
                        this.m_JsonSerializer.WriteObject(buffer, bodyObject);

                        break;
                }

                msg.Properties.Add(RestMessageProperty.Name, new RestMessageProperty(buffer));
            }

            HttpRequestMessageProperty request = new HttpRequestMessageProperty();
            request.Method = this.m_HttpMethod;
            if (suppressBody)
            {
                request.SuppressEntityBody = true;
            }

            if (!suppressBody && !String.IsNullOrEmpty(this.m_ContentType))
            {
                request.Headers[HttpRequestHeader.ContentType] = this.m_ContentType;
            }

            if (!String.IsNullOrEmpty(this.m_QueryStringTemplate))
            {
                request.QueryString = String.Format(this.m_QueryStringTemplate, parameters);
            }

            msg.Properties.Add(HttpRequestMessageProperty.Name, request);

            if (!String.IsNullOrEmpty(this.m_UrlTemplate))
            {
                msg.Headers.To = new Uri(this.m_EndpointAddress + String.Format(this.m_UrlTemplate, parameters));
            }
            else
            {
                msg.Headers.To = new Uri(this.m_EndpointAddress);
            }

            return msg;
        }

        #region Initialization

        private void SetUrlTemplate(String url)
        {
            if (!String.IsNullOrEmpty(url))
            {
                String[] parts = url.Split(new Char[] { '?' }, StringSplitOptions.None);

                this.m_UrlTemplate = parts[0];

                if (parts.Length > 1)
                {
                    this.m_QueryStringTemplate = String.Join("?", parts.Skip(1));
                }
            }
        }

        private void SetSerializationMethod(Type type, RestFormat restFormat)
        {
            switch (restFormat)
            {
                case RestFormat.Xml:
                    if (String.IsNullOrEmpty(this.m_ContentType))
                    {
                        this.m_ContentType = "application/json";
                    }

                    if (typeof(IXmlSerializable).IsAssignableFrom(type))
                    {
                        this.m_Serialization = SerializationMethod.IXmlSerializable;
                    }
                    else
                    {
                        if (type.GetCustomAttributes(typeof(DataContractAttribute), true).Length > 0)
                        {
                            this.m_Serialization = SerializationMethod.XmlDataContract;
                            this.m_DataContractSerializer = new DataContractSerializer(type);
                        }
                        else
                        {
                            this.m_Serialization = SerializationMethod.XmlSerializable;
                            this.m_XmlSerializer = new XmlSerializer(type);
                        }
                    }

                    break;
                case RestFormat.Json:
                    if (String.IsNullOrEmpty(this.m_ContentType))
                    {
                        this.m_ContentType = "application/xml";
                    }

                    this.m_Serialization = SerializationMethod.JsonDataContract;
                    this.m_JsonSerializer = new DataContractJsonSerializer(type);

                    break;
            }
        }

        private void SetDeserializationMethod(Type type, RestFormat restFormat)
        {
            switch (restFormat)
            {
                case RestFormat.Xml:
                    if (typeof(IXmlSerializable).IsAssignableFrom(type))
                    {
                        this.m_Deserialization = SerializationMethod.IXmlSerializable;
                    }
                    else
                    {
                        if (type.GetCustomAttributes(typeof(DataContractAttribute), true).Length > 0)
                        {
                            this.m_Deserialization = SerializationMethod.XmlDataContract;
                            this.m_DataContractDeserializer = new DataContractSerializer(type);
                        }
                        else
                        {
                            this.m_Deserialization = SerializationMethod.XmlSerializable;
                            this.m_XmlDeserializer = new XmlSerializer(type);
                        }
                    }

                    break;
                case RestFormat.Json:
                    this.m_Deserialization = SerializationMethod.JsonDataContract;
                    this.m_JsonDeserializer = new DataContractJsonSerializer(type);

                    break;
            }
        }

        #endregion

        private enum SerializationMethod
        {
            IXmlSerializable,
            XmlSerializable,
            XmlDataContract,
            JsonDataContract
        }
    }
}
